package net.sf.odinms.exttools.doclet;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.jar.JarFile;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.RootDoc;
import com.sun.tools.doclets.internal.toolkit.Configuration;
import com.sun.tools.doclets.internal.toolkit.WriterFactory;
import com.sun.tools.doclets.internal.toolkit.util.ClassTree;
import com.sun.tools.doclets.internal.toolkit.util.ClassUseMapper;
import com.sun.tools.doclets.internal.toolkit.util.DirectoryManager;
import com.sun.tools.doclets.internal.toolkit.util.IndexBuilder;
import com.sun.tools.doclets.internal.toolkit.util.MessageRetriever;
import net.sf.odinms.exttools.doclet.writers.WriterFactoryImpl;

/**
 * <p>
 * Configures the output based on the supplied command-line options.
 * </p>
 * <p>
 * Also does validation of specified options. For String arguments, the default
 * is "", and for boolean arguments, the default is false, except where
 * explicitly noted.
 * </p>
 * 
 * @see com.sun.tools.doclets.formats.html.ConfigurationImpl
 */
@SuppressWarnings("unchecked")
public class Config extends Configuration {
	
	private static final Config INSTANCE = new Config();
	
	private static final String PROPERTIES_LOCATION =
			"net.sf.odinms.exttools.doclet.resources.doclet";
	
	/** The Java version number currently installed. */
	public static final String JAVA_VERSION =
			System.getProperty("java.version");
	
	/** File extension to use for Javadoc-generated files. */
	public String ext = ".html";
	
	// FIXME Use private maps for command-line booleans and args.
	// e.g. Map<String, boolean> and Map <String, String>
	
	/** Argument for command-line option "-header". */
	public String header = "";
	
	/** Argument for command-line option "-packagesheader". */
	public String packagesheader = "";
	
	/** Argument for command-line option "-footer". */
	public String footer = "";
	
	/** Argument for command-line option "-doctitle". */
	public String doctitle = "";
	
	/** Argument for command-line option "-windowtitle". */
	public String windowtitle = "";
	
	/** Argument for command-line option "-bottom". */
	public String bottom = "";
	
	/** Argument for command-line option "-helpfile". */
	public String helpfile = "";
	
	/** Argument for command-line option "-stylesheetfile". */
	public String stylesheetfile = "";
	
	/** True if command-line option "-nohelp" is used. */
	public boolean nohelp = false;
	
	/** True if command-line option "-noindex" is used. */
	private boolean noindex = false;
	
	/** True if command-line option "-plainindex" is used. */
	public boolean plainindex = false;
	
	/** True if command-line option "-splitindex" is used. */
	public boolean splitindex = false;
	
	/** False if command-line option "-noindex" is used. Default value is true. */
	public boolean createindex = true;
	
	/** True if command-line option "-use" is used. */
	public boolean use = false;
	
	/** False if command-line option "-notree" is used. Default value is true. */
	public boolean createtree = true;
	
	/** True if command-line option "-nodeprecated" is used. */
	public boolean nodeprecatedlist = false;
	
	///** True if command-line option "-nonavbar" is used. */
	//private boolean nonavbar = false;
	
	/** True if command-line option "-nooverview" is used. */
	private boolean nooverview = false;
	
	/** True if command-line option "-overview" is used. */
	private boolean overview = false;
	
	/** True if "-overview" is used or number of packages is greater than one. */
	public boolean createoverview = false;
	
	/**
	 * The maximum number of package directories shown in the constant value
	 * index.
	 */
	public int constantListMaxPackageComponents = 2;
	
	/** Unique Resource Handler for this package. */
	public final MessageRetriever standardmessage;
	
	/** Object for grouping symbols for the Javadoc index page. */
	public IndexBuilder indexBuilder;
	
	/**
	 * First file to appear in the right-hand frame in the generated
	 * documentation.
	 */
	public String topFile = "";
	
	/**
	 * The starting filename of the API symbol index.
	 */
	public String rootIndexPage = "";
	
	/** The classdoc for the class file getting generated. */
	public ClassDoc currentClass = null;
	// Set this ClassDoc in the ClassWriter.
	
	/** The package currently being processed. */
	public PackageDoc currentPkg = null;
	
	/**
	 * Contains hierarchies for classes and interfaces. Used in creating package
	 * trees, as well as for identifying subclasses, implementing classes, and
	 * subinterfaces in class detail pages.
	 */
	public ClassTree classTree = null;
	
	/** List of all included classes for easy reference of inclusion or not. */
	public List classList = null;
	
	/** Map from classes to usage of those classes elsewhere. */
	public ClassUseMapper classUseMapper;
	
	private ResourceBundle messageRB;
	
	/**
	 * Private constructor; initializes resources for the
	 * {@link MessageRetriever}.
	 * 
	 * @see ResourceBundle#getBundle(String,Locale,ClassLoader)
	 */
	protected Config() {
		standardmessage = new MessageRetriever(this, PROPERTIES_LOCATION);
		
		try {
			messageRB = ResourceBundle.getBundle(PROPERTIES_LOCATION);
		}
		catch (MissingResourceException e) {
			throw new Error("Fatal: Resource (" + PROPERTIES_LOCATION
					+ ") for javadoc doclets is missing.");
		}
		
	}
	
	/**
	 * Get a static (singleton) INSTANCE of this class.
	 * 
	 * @return A DocletConfig object representing the INSTANCE value.
	 */
	public static Config getInstance() {
		return INSTANCE;
	}
	
	/**
	 * Return the doclet build date, fetched from "resources/build_date.txt".
	 * 
	 * @return A date in ISO format (YYYY-MM-DD), null if file does not exist.
	 */
	public String getDocletSpecificBuildDate() {
		return readFromResourceFile("build_date.txt").trim();
	}
	
	/**
	 * Read the revision from a file in resources.
	 * 
	 * @return The Subversion revision that was used to build this version.
	 */
	public String getDocletSpecificRevision() {
		String revision = readFromResourceFile("revision.txt").trim();
		if (revision.contains(":"))
			return revision.substring(revision.indexOf(":") + 1);
		else
			return revision;
	}
	
	/**
	 * Read data from a resource file, and return it as a string.
	 * 
	 * @param filename The name of the resource file to read from.
	 * @return A string representing the contents of the read file.
	 */
	private String readFromResourceFile(String filename) {
		StringBuffer buffer = new StringBuffer();
		try {
			InputStreamReader reader =
					new InputStreamReader(getClass().getResourceAsStream(
							"resources/" + filename));
			while (reader.ready()) {
				buffer.append((char) reader.read());
			}
		}
		catch (IOException exc) {
			return null;
		}
		return buffer.toString();
	}
	
	/**
	 * Depending upon the command-line options provided by the user, configure
	 * the output generation environment.
	 * 
	 * @param options The array of option names and values.
	 */
	public void setSpecificDocletOptions(String[][] options) {
		for (int oi = 0; oi < options.length; oi++) {
			String[] os = options[oi];
			String opt = os[0].toLowerCase();
			if (opt.equals("-footer")) {
				footer = os[1];
			} else if (opt.equals("-header")) {
				header = os[1];
			} else if (opt.equals("-packagesheader")) {
				packagesheader = os[1];
			} else if (opt.equals("-doctitle")) {
				doctitle = os[1];
			} else if (opt.equals("-windowtitle")) {
				windowtitle = os[1];
			} else if (opt.equals("-bottom")) {
				bottom = os[1];
			} else if (opt.equals("-helpfile")) {
				helpfile = os[1];
			} else if (opt.equals("-stylesheetfile")) {
				stylesheetfile = os[1];
			} else if (opt.equals("-charset")) {
				charset = os[1];
			} else if (opt.equals("-nohelp")) {
				nohelp = true;
			} else if (opt.equals("-splitindex")) {
				splitindex = true;
			} else if (opt.equals("-noindex")) {
				createindex = false;
			} else if (opt.equals("-use")) {
				use = true;
			} else if (opt.equals("-notree")) {
				createtree = false;
			} else if (opt.equals("-nodeprecatedlist")) {
				nodeprecatedlist = true;
			} else if (opt.equals("-nosince")) {
				nosince = true;
			} else if (opt.equals("-nooverview")) {
				nooverview = true;
			} else if (opt.equals("-overview")) {
				overview = true;
			}
//			else if (opt.equals("-nonavbar")) {
//				nonavbar = true;
//			}
		}
		if (root.specifiedClasses().length > 0) {
			Map<String, PackageDoc> map = new HashMap<String, PackageDoc>();
			PackageDoc pd;
			ClassDoc[] classes = root.classes();
			for (int i = 0; i < classes.length; i++) {
				pd = classes[i].containingPackage();
				if (!map.containsKey(pd.name())) {
					map.put(pd.name(), pd);
				}
			}
			// what happens to the map after the packages are inserted?
		}
		setCreateOverview();
		setTopFile(root);
	}
	
	private final List<String> optionsLength1 =
			new ArrayList<String>(Arrays.asList(new String[] {"-help",
					"-nodeprecatedlist", "-noindex", "-notree", "-nohelp",
					"-splitindex", "-serialwarn", "-use", "-nonavbar",
					"-nooverview"}));
	
	private final List<String> optionsLength2 =
			new ArrayList<String>(Arrays.asList(new String[] {"-footer",
					"-header", "-packagesheader", "-doctitle", "-windowtitle",
					"-bottom", "-helpfile", "-stylesheetfile", "-charset",
					"-overview"}));
	
	/**
	 * Returns the "length" of a given option. If an option takes no arguments,
	 * its length is one. If it takes one argument, it's length is two, and so
	 * on. This method is called by JavaDoc to parse the options it does not
	 * recognize. It then calls
	 * {@link #validOptions(String[][], DocErrorReporter)} to validate them.
	 * <p>
	 * <b>Note:</b><br>
	 * The options arrive as case-sensitive strings. For options that are not
	 * case-sensitive, use toLowerCase() on the option string before comparing
	 * it.
	 * </p>
	 * 
	 * @param option The text of a command-line option provided to the doclet.
	 * @return Number of arguments + 1 for an option. Zero return means option
	 *         not known. Negative value means error occurred.
	 */
	public int optionLength(String option) {
		int optionLength = super.optionLength(option);
		if (option.equals("-help"))
			System.out.println(getText("doclet.usage"));
		if (optionLength > 0) {
			return optionLength;
		}
		// otherwise look for the options we have added
		option = option.toLowerCase();
		if (optionsLength2.contains(option))
			optionLength = 2;
		else if (optionsLength1.contains(option))
			optionLength = 1;
		else
			optionLength = 0;
		return optionLength;
	}
	
	/**
	 * <p>
	 * Check that options have the correct arguments.
	 * </p>
	 * <p>
	 * This method is responsible for printing option-related error messages,
	 * using the provided DocErrorReporter.
	 * </p>
	 * <p>
	 * Determines the length of the command-line option. For example, there is
	 * an associated string argument for the "-header" option, so its length is
	 * 2. However, the "-nohelp" option has no arguments, so its length is 1.
	 * </p>
	 * <p>
	 * Also does error checking on the options used. For example, it is illegal
	 * to use "-helpfile" together with "-nohelp".
	 * </p>
	 * 
	 * @param options A 2-dimensional array of options to check for validity.
	 *        Each of the first-order elements represents a single option, and
	 *        contains an array with the arguments corresponding to it.
	 * @param reporter An object for printing errors, notices, and warnings.
	 * @return true if the options are valid.
	 */
	public boolean validOptions(String[][] options, DocErrorReporter reporter) {
		boolean helpfile = false;
		boolean errorCondition = false;
		// check shared options
		if (!generalValidOptions(options, reporter)) {
			return false;
		}
		// otherwise look at our options
		for (int oi = 0; oi < options.length; ++oi) {
			String[] os = options[oi];
			String opt = os[0].toLowerCase();
			if (opt.equals("-nohelp")) {
				if (helpfile) {
					reporter.printError(getText("doclet.Option_conflict",
							"-nohelp", "-helpfile"));
					errorCondition = true;
					break;
				}
				nohelp = true;
			} else if (opt.equals("-helpfile")) {
				if (nohelp) {
					reporter.printError(getText("doclet.Option_conflict",
							"-helpfile", "-nohelp"));
					errorCondition = true;
					break;
				}
				if (helpfile) {
					reporter.printError(getText("doclet.Option_reuse",
							"-helpfile"));
					errorCondition = true;
					break;
				}
				File help = new File(os[1]);
				if (!help.exists()) {
					reporter.printError(getText("doclet.File_not_found", os[1]));
					errorCondition = true;
					break;
				}
				helpfile = true;
			} else if (opt.equals("-overview")) {
				if (nooverview) {
					reporter.printError(getText("doclet.Option_conflict",
							"-overview", "-nooverview"));
					errorCondition = true;
					break;
				}
				if (overview) {
					reporter.printError(getText("doclet.Option_reuse",
							"-overview"));
					errorCondition = true;
					break;
				}
				overview = true;
			} else if (opt.equals("-nooverview")) {
				if (overview) {
					reporter.printError(getText("doclet.Option_conflict",
							"-nooverview", "-overview"));
					errorCondition = true;
					break;
				}
				nooverview = true;
			} else if (opt.equals("-plainindex")) {
				if (noindex) {
					reporter.printError(getText("doclet.Option_conflict",
							"-plainindex", "-noindex"));
					errorCondition = true;
					break;
				}
				plainindex = true;
			} else if (opt.equals("-splitindex")) {
				if (noindex) {
					reporter.printError(getText("doclet.Option_conflict",
							"-splitindex", "-noindex"));
					errorCondition = true;
					break;
				}
				splitindex = true;
			} else if (opt.equals("-noindex")) {
				if (splitindex) {
					reporter.printError(getText("doclet.Option_conflict",
							"-noindex", "-splitindex"));
					errorCondition = true;
					break;
				}
				if (plainindex) {
					reporter.printError(getText("doclet.Option_conflict",
							"-noindex", "-plainindex"));
					errorCondition = true;
					break;
				}
				noindex = true;
			}
		}
		return !errorCondition;
	}
	
	/**
	 * {@inheritDoc}.
	 */
	public MessageRetriever getDocletSpecificMsg() {
		return standardmessage;
	}
	
	// public void notice(String, String) {
	//		
	// }
	
	/**
	 * Decide the page which will appear first in the right-hand frame. It will
	 * be "overview-summary.html" if "-overview" option is used or no
	 * "-overview" but the number of packages is more than one. It will be
	 * "package-summary.html" of the respective package if there is only one
	 * package to document. It will be a class page(first in the sorted order),
	 * if only classes are provided on the command-line.
	 * 
	 * @param root Root of the program structure.
	 */
	protected void setTopFile(RootDoc root) {
		if (!checkForDeprecation(root)) {
			return;
		}
		classList = java.util.Arrays.asList(root.classes());
		if (createoverview) {
			topFile = "overview-summary.html";
		} else {
			if (packages.length == 1 && packages[0].name().equals("")) {
				if (root.classes().length > 0) {
					ClassDoc[] classarr = root.classes();
					Arrays.sort(classarr);
					ClassDoc cd = getValidClass(classarr);
					topFile = DirectoryManager.getPathToClass(cd);
				}
			} else {
				topFile =
						DirectoryManager.getPathToPackage(packages[0],
								"package-summary.html");
			}
		}
	}
	
	/**
	 * Given an array of classes, return the first valid class.
	 * 
	 * @param classArray An array of ClassDoc objects to check for deprecation.
	 * @return The first class in the array if the <code>-nodeprecated</code>
	 *         option is used; the first non-deprecated class otherwise. In the
	 *         second case, if no class is non-deprecated, return null.
	 */
	protected ClassDoc getValidClass(ClassDoc[] classArray) {
		ClassDoc firstValidClass = null;
		if (!nodeprecated)
			firstValidClass = classArray[0];
		else {
			for (int i = 0; i < classArray.length; i++) {
				if (classArray[i].tags("deprecated").length == 0) {
					firstValidClass = classArray[i];
					break;
				}
			}
		}
		return firstValidClass;
	}
	
	/**
	 * Check whether there are any deprecated members.
	 * 
	 * @param root Root of the parsed program structure.
	 * @return True if -nodeprecated option and "@deprecated" tag are not used.
	 */
	protected boolean checkForDeprecation(RootDoc root) {
		ClassDoc[] classarr = root.classes();
		for (int i = 0; i < classarr.length; i++) {
			if (isGeneratedDoc(classarr[i])) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Generate "overview.html" page if option "-overview" is used or number of
	 * packages is more than one. Sets {@link #createoverview} field to true.
	 */
	protected void setCreateOverview() {
		if ((overview || packages.length > 1) && !nooverview) {
			createoverview = true;
		}
	}
	
	/**
	 * {@inheritDoc}.
	 */
	public WriterFactory getWriterFactory() {
		return WriterFactoryImpl.getInstance();
	}
	
	/**
	 * {@inheritDoc}.
	 */
	public Comparator getMemberComparator() {
		return null;
	}
	
	/**
	 * <p>
	 * Retrieve and format a message string from resource for <code>key</code>.
	 * Accepts a reference to a property format string and 0 or more arguments.
	 * (Replaces 4 versions of <code>getText()</code>.)
	 * </p>
	 * <p>
	 * NOTE: May throw an unchecked exceptions which will be caught later.
	 * </p>
	 * 
	 * @see MissingResourceException
	 * @see IllegalArgumentException
	 * 
	 * @param key A string representing the property to fetch.
	 * @param values Value objects for place-holders in <code>key</code>.
	 * @return A value-substituted string for the resource <code>key</code>.
	 */
	public String propertyText(String key, Object... values) {
		if (key == null)
			return null;
		return java.text.MessageFormat.format(messageRB.getString(key), values);
	}
	
	/**
	 * Finds the JAR file enclosing the current class. (If an enclosing JAR does
	 * not exist, the project is being executed directly from class files.)
	 * 
	 * @return A {@link JarFile} object for the enclosing JAR, if one exists. If
	 *         any error occurs, null is returned.
	 */
	public JarFile enclosingJarFile() {
		try {
			String thisClass =
					"/" + getClass().getName().replace('.', '/') + ".class";
			java.net.URL jarUrl = this.getClass().getResource(thisClass);
			String stringForm = jarUrl.toString();
			
			int endIdx = stringForm.indexOf("!/");
			if (endIdx == -1)
				throw new FileNotFoundException(
						"Could not locate enclosing jar: !/ not found in URL");
			
			String fileNamePart =
					stringForm.substring("jar:file:".length(), endIdx);
			String unescaped =
					java.net.URLDecoder.decode(fileNamePart,
							System.getProperty("file.encoding"));
			File file = new File(unescaped);
			if (!file.exists())
				throw new FileNotFoundException(
						"Could not locate enclosing jar: " + file
								+ " not found");
			return new JarFile(file);
		}
		catch (FileNotFoundException exc) {
			System.out.println(exc.getMessage());
		}
		catch (IOException exc) {
			System.out.println(exc.getMessage());
		}
		return null;
	}
	
}
